Skip to content

fix(execution): 레드팀 치명 2건 수정 + v1.9.0 (STO-1729, STO-1730) - #55

Merged
unohee merged 2 commits into
mainfrom
feat/sto-1728-twap-vwap-execution
Aug 21, 2026
Merged

fix(execution): 레드팀 치명 2건 수정 + v1.9.0 (STO-1729, STO-1730)#55
unohee merged 2 commits into
mainfrom
feat/sto-1728-twap-vwap-execution

Conversation

@unohee

@unohee unohee commented Aug 21, 2026

Copy link
Copy Markdown
Owner

왜 이 PR이 필요한가

PR #54가 2b70201에서 머지되면서 TWAP/VWAP 기능이 치명 결함 2건을 안은 채 main에 들어갔다. 현재 main(094c629)에는 주문 재전송 차단도, 집행 원장도 없다. 이 PR이 그 둘을 넣는다.

두 결함 모두 openswarm review(커밋 전)와 openswarm pr review --fresh(PR)를 통과한 뒤 /redteam이 찾아냈다. 앞선 리뷰가 틀린 게 아니다 — "monotonic 앵커가 올바르다", "dry-run이 주문을 차단한다"는 프로세스가 살아 있고 사람이 보고 있다는 전제 위에서만 참이었고, 레드팀은 그 전제를 때렸다.

STO-1729 — 주문은 절대 재전송하지 않는다

client.make_request의 기본 retries=2가 주문 POST에도 적용됐다. 타임아웃은 응답에 걸린 것이지 동작에 걸린 것이 아니다 — 거래소에 도달해 접수된 주문의 응답만 유실됐는데 같은 본문을 다시 보내면 중복 주문이 된다. KIS는 멱등키를 받지 않아 거래소가 걸러줄 수도 없다. 단발 주문은 주사위를 1번 굴리지만 TWAP은 슬라이스마다, 아무도 안 보는 30~120분 동안 굴린다.

이제 GET이 아닌 요청은 전송 1회로 고정된다.

판정을 > 1이 아니라 != 1로 한 이유: 전자는 retries=0을 통과시켜 range(0) 루프가 아예 돌지 않고, 주문이 나가지 않은 채 "Unknown error after retries"로 끝난다 — 호출자가 전송 실패와 구분할 수 없다. (커밋 전 리뷰가 지적)

패키지의 POST 16곳은 전부 */order_api.py이고 토큰 발급은 requests.post를 직접 써서 이 경로를 타지 않는다. 이 전제가 깨지면 먼저 알도록 테스트로 고정했다.

STO-1730 — 집행 원장

SIGTERM 8초 실측: stdout 0바이트, stderr에만 이미 실행된 슬라이스 흔적. 실주문이었다면 나간 주문번호를 복구할 방법이 없다.

이제 자식 주문은 거래소 접수 직후 JSONL 원장에 flush + fsync된다.

~/.kis-agent/executions/20260821/20260821-133000-005930-buy-3f9a2c.jsonl

SIGKILL 서브프로세스 테스트로 핸들러 없는 즉사에도 주문번호가 디스크에 남는 것을 직접 증명한다 (test_order_numbers_survive_an_unhandled_kill).

미완료 원장(=죽은 실행)이 같은 종목에 있으면 새 집행을 거부한다. 가드는 CLI가 아니라 runner에 뒀다 — CLI만 막고 문서화된 Python API를 열어두면 같은 치명이 그대로 남는다 (커밋 전 리뷰가 지적). IncompleteExecutionError로 올라오고 check_incomplete=False로 opt-out 한다.

가드가 걸리는 것은 처리되지 않은 즉사뿐이다. 실측 확인:

경로 end 레코드 다음 집행
정상 완료 있음 통과
Ctrl+C (의도적 중단) 있음 통과
SIGKILL/OOM/하네스 타임아웃 없음 차단
어제 죽은 실행 없음 통과 (당일 범위)

당일 범위로 둔 이유는 KRX 당일 주문이 장 마감을 넘기지 못하는 데다, 한 번의 크래시가 그 종목을 영구히 막으면 안 되기 때문이다. 다만 부분 체결 포지션은 남으므로 크래시 후 잔고 확인이 필요하다는 점을 문서에 명시했다.

원장 기록 실패는 주문을 중단시키지 않는다 — 디스크가 찼다고 절반 집행된 부모 주문을 버리는 것이 더 나쁘다. 그래서 주문번호를 stderr 진행 출력에도 함께 싣는다.

릴리즈

1.8.0 → 1.9.0. CHANGELOG의 [Unreleased] 마감, python -m build + twine check 통과.

검증

  • 신규/수정 모듈 100% 커버리지, 단위 테스트 241개
  • 전체 스위트 회귀 0건 — 51 failed/16 errors는 로컬 py3.14 환경의 기존 문제로 변경 전과 동일 (git stash 대조 확인). passed 2133 → 2190
  • 테스트가 ~/.kis-agent에 실제 원장 65개를 흘리던 것을 발견해 격리 픽스처 추가

남은 중대 2건 (별도 이슈, 이 PR 범위 밖)

  • STO-1731 — 접수/체결 용어 충돌(filled vs filledQty), dry-run 신호 모호, 마감 초과 무경고
  • STO-1732 — 1급 호출자(LLM)에게 인간 승인 게이트가 구조적으로 부재

Closes STO-1729
Closes STO-1730

🤖 Generated with Claude Code

/redteam이 서로 직교하는 두 축(L3 운영 현실, L4 호출자 정체성)에서 치명 2건을
냈다. 둘 다 코드 리뷰 두 층(openswarm review, pr review)을 통과한 뒤에 나왔다 —
그 리뷰들이 틀린 게 아니라, "프로세스가 살아 있고 사람이 보고 있다"는 전제
위에서만 맞는 말이었기 때문이다.

STO-1729 — 주문은 절대 재전송하지 않는다
  client.make_request의 기본 retries=2가 주문 POST에도 적용됐다. 타임아웃은
  응답에 걸린 것이지 동작에 걸린 것이 아니다. 접수된 주문의 응답만 유실됐는데
  같은 본문을 다시 보내면 중복 주문이 되고, KIS는 멱등키를 받지 않아 거래소가
  걸러줄 수도 없다. 이제 GET이 아닌 요청은 전송 1회로 고정된다.

  `retries > 1`이 아니라 `!= 1`로 판정한다. 전자는 retries=0을 통과시켜
  range(0) 루프가 아예 돌지 않고, 주문이 나가지 않은 채 "Unknown error"로
  끝난다 — 호출자는 그걸 전송 실패와 구분할 수 없다. (커밋 전 리뷰 지적)

  패키지의 POST 16곳은 전부 */order_api.py이고 토큰 발급은 requests.post를
  직접 쓴다. 이 전제가 깨지면 먼저 알도록 테스트로 고정했다.

STO-1730 — 집행 원장
  TWAP/VWAP은 30~120분 블로킹이다. 그 사이 프로세스가 죽으면(SIGKILL·절전·
  OOM·에이전트 타임아웃) 나간 주문번호가 메모리와 함께 사라졌다. 실측으로
  SIGTERM 8초에 stdout 0바이트, stderr에는 이미 실행된 슬라이스 흔적만 남았다.

  이제 자식 주문은 거래소 접수 직후 JSONL 원장에 flush + fsync된다. SIGKILL
  서브프로세스 테스트로 "핸들러 없는 즉사에도 주문번호가 디스크에 남는다"를
  직접 증명한다.

  미완료 원장(=죽은 실행)이 같은 종목에 있으면 새 집행을 거부한다. 가드는
  CLI가 아니라 runner에 둔다 — CLI만 막고 문서화된 Python API를 열어두면
  같은 치명이 그대로 남는다. (커밋 전 리뷰 지적)

  가드가 걸리는 것은 처리되지 않은 즉사뿐이다. 정상 완료와 Ctrl+C는 원장을
  닫으므로 통과하고, 당일 범위라 한 번의 크래시가 종목을 영구히 막지 않는다.
  실측으로 세 경로 모두 확인했다.

원장 기록 실패는 주문을 중단시키지 않는다. 디스크가 찼다고 절반 집행된 부모
주문을 버리는 것이 더 나쁘다. 그래서 주문번호를 stderr 진행 출력에도 싣는다.

검증
- 신규/수정 모듈 100% 커버리지, 단위 테스트 241개
- 전체 스위트 회귀 0건 (51 failed/16 errors는 로컬 py3.14 환경의 기존 문제로
  변경 전과 동일). passed 2133 → 2190
- 테스트가 ~/.kis-agent에 실제 원장 65개를 흘리던 것을 발견해 격리 픽스처 추가

릴리즈
- 1.8.0 → 1.9.0, CHANGELOG의 Unreleased 마감
- python -m build + twine check 통과 (wheel/sdist)
@unohee

unohee commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🔍 Fresh review of 2a57e01 (openswarm pr review --fresh)

✎ Decision: REVISE
커밋 diff와 변경 파일을 읽어 주문 안전성·집행 원장 변경을 검토합니다.

openswarm pr review가 Decision: REVISE를 냈다. 트레이스 본문은 "approval"이라
썼지만 구조화된 판정 줄이 정본이다.

가드가 종목 단위라 크래시한 매수가 청산 매도까지 막았다. 사고 직후 운영자가
가장 하고 싶은 일이 청산인데 도구가 그걸 방해하면, 가드가 있는 것이 없는 것보다
나쁘다. 중복 위험은 어차피 같은 방향에서 생긴다. 이제 같은 방향만 막는다.

문서가 "정상 종료와 Ctrl+C는 원장을 닫는다"고 주장하면서 테스트가 없었다.
수동으로 확인했을 뿐 고정하지 않은 것은 주장이지 보장이 아니다. cancelled·
aborted·partial 세 경로와 스킵 슬라이스 감사 기록까지 테스트로 고정했다.

가드가 보여주는 주문번호가 적게 나올 수 있다는 점도 명시했다 — 재전송을 하지
않으므로 응답이 유실된 주문은 접수됐더라도 failed로 기록된다. 대사의 출발점이지
완전한 목록이 아니고, kis order list가 정본이다.

리뷰가 지적한 검사-기록 사이 경합 창은 STO-1733으로 분리했다. 창이 밀리초이고
가드의 표적인 순차 재실행은 완전히 막히며, O_CREAT|O_EXCL 락이 필요한 별개
작업이다.

리뷰의 "retries 기본값이 정말 5에서 2로 줄었나" 확인 요청은 확인 결과 조치
불필요다 — 코드는 이미 2였고 docstring만 5라고 잘못 적혀 있었다. 이번에 맞췄다.

검증: 신규/수정 모듈 100% 커버리지, 회귀 0건 (passed 2190 → 2201)
@unohee

unohee commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

리뷰 대응 (6111f21)

openswarm pr review --fresh 판정은 **Decision: REVISE**였다. 트레이스 본문은 "the reviewer will approve the changes"라고 썼지만 구조화된 판정 줄이 정본이라 그쪽을 따랐다.

지적 판정 처리
가드가 종목 단위라 크래시한 매수가 청산 매도까지 막는다 실재·운영 결함 수정. 같은 방향만 막는다
문서는 "Ctrl+C가 원장을 닫는다"고 하는데 테스트가 없다 실재 수정. cancelled·aborted·partial + 스킵 슬라이스 감사까지 고정
order_numbers가 실제보다 적을 수 있다 (failed 슬라이스가 실은 접수됐을 수 있음) 실재 문서화. docstring·문서·오류 메시지에서 kis order list가 정본임을 명시
검사와 start 기록 사이 경합 창 실재·경미 분리 → STO-1733
retries 기본값이 정말 5→2로 줄었나 조치 불필요 코드는 이미 2였고 docstring만 5라고 틀려 있었다. 이번에 맞춤
커스텀 journal-dir이 기본 디렉터리의 옛 기록을 못 본다 조치 불필요 CLI가 검사와 실행에 같은 디렉터리를 쓴다 (리뷰도 user misconfiguration으로 판단)
주문 0건인 미완료 실행도 막는다 의도된 보수성 describe()가 "주문번호 없음"으로 표시하므로 운영자가 판단 가능

side 스코핑이 왜 옳은가

중복 위험은 같은 방향에서 생긴다. 반대 방향은 사고의 해법이다 — 크래시한 매수 이후 가장 하고 싶은 일이 청산인데 도구가 그걸 막으면, 가드가 있는 것이 없는 것보다 나쁘다.

상황 이전 지금
크래시한 매수 → 새 매수 차단 차단
크래시한 매수 → 청산 매도 차단 (해로움) 통과

검증

  • 신규/수정 모듈 100% 커버리지 유지, 단위 테스트 252개
  • 회귀 0건 — 51 failed/16 errors는 로컬 py3.14 환경의 기존 문제로 변경 전과 동일. passed 2190 → 2201

@unohee
unohee merged commit ee6c2af into main Aug 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant